use sqlx::{Connection, Executor, PgConnection, PgPool, migrate::Migrator};
use std::{path::Path, thread};
use tokio::runtime::Runtime;
use uuid::Uuid;

pub struct TestDb {
    pub host: String,
    pub port: u16,
    pub username: String,
    pub password: String,
    pub dbname: String,
}

impl TestDb {
    pub fn new(
        host: impl Into<String>,
        port: u16,
        username: impl Into<String>,
        password: impl Into<String>,
        migration_path: impl Into<String>,
    ) -> Self {
        let uuid = Uuid::new_v4();
        let dbname = format!("test_{}", uuid);
        let dbname_clone = dbname.clone();

        let host = host.into();
        let username = username.into();
        let password = password.into();

        let tdb = Self { host, port, username, password, dbname };

        let server_url = tdb.db_server_url();
        let url = tdb.url();
        let migration_path = migration_path.into();

        // 建立数据库
        thread::spawn(move || {
            let rt = Runtime::new().unwrap();
            rt.block_on(async move {
                // 先连接配置文件中的数据库
                let mut conn = PgConnection::connect(&server_url).await.unwrap();
                conn.execute(format!(r#"CREATE DATABASE "{}""#, dbname_clone).as_str())
                    .await
                    .unwrap();

                // 再连接到刚创建的数据库
                let mut conn = PgConnection::connect(&url).await.unwrap();

                // 执行 migrate
                let m = Migrator::new(Path::new(&migration_path)).await.unwrap();
                m.run(&mut conn).await.unwrap();
            });
        })
        .join()
        .expect("failed to create database");

        tdb
    }

    fn db_server_url(&self) -> String {
        if self.password.is_empty() {
            format!("postgres://{}@{}:{}", self.username, self.host, self.port)
        } else {
            format!("postgres://{}:{}@{}:{}", self.username, self.password, self.host, self.port)
        }
    }

    pub fn url(&self) -> String {
        format!("{}/{}", self.db_server_url(), self.dbname)
    }

    pub async fn get_pool(&self) -> PgPool {
        sqlx::postgres::PgPoolOptions::new().max_connections(10).connect(&self.url()).await.unwrap()
    }
}

// 重写 Drop trait，方法会自动调用
impl Drop for TestDb {
    // 由于 drop 接口不能使用 async，所以需要使用同步的接口，一个异步的接口想要变成同步的接口的话，只能使用 Runtime::block_on，
    // 而如果直接使用的这个，就会产生 runtime inside runtime的错误，所以在外层创建一个新的 thread::spawn 去 runtime 这个确保这个方法结束
    fn drop(&mut self) {
        let server_url = self.db_server_url();
        let dbname = self.dbname.clone();
        thread::spawn(move || {
            let rt = Runtime::new().unwrap();
            rt.block_on(async move {
                let mut conn = PgConnection::connect(&server_url).await.unwrap();
                // 强制把对 dbname 数据库的连接全部关闭
                conn.execute(format!(r#"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid <> pg_backend_pid() AND datname = '{}'"#, dbname).as_str()).await.expect("Terminate all other connections");
                conn.execute(format!(r#"DROP DATABASE "{}""#, dbname).as_str()).await.expect("Error while querying the drop database");
            });
        }).join().expect("failed to drop database")
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_db_should_create_and_drop() {
        let tdb = TestDb::new("47.102.206.75", 5432, "postgres", "wzj@123456", "./migrations");
        let pool = tdb.get_pool().await;

        sqlx::query("INSERT INTO todos(title) VALUES ('test')").execute(&pool).await.unwrap();
        let (id, title): (i32, String) =
            sqlx::query_as("SELECT id, title FROM todos").fetch_one(&pool).await.unwrap();
        assert_eq!(id, 1);
        assert_eq!(title, "test");
    }
}
